﻿#region File and License Information
/*
<File>
	<License>
		Copyright © 2009 - 2017, Daniel Vaughan. All rights reserved.
		This file is part of Calcium (http://calciumsdk.net), 
		which is released under the MIT License.
		See file /Documentation/License.txt for details.
	</License>
	<CreationDate>2009-03-01 18:44:00Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Security;
using System.Threading;

using Outcoder.Concurrency;
using Outcoder.Logging;

namespace Outcoder.ServiceModel
{
	public static class InstanceIdHeader
	{
		public static readonly string HeaderName = "InstanceId";
		public static readonly string HeaderNamespace = OrganizationalConstants.ServiceContractNamespace;
	}

	/// <summary>
	/// Default implementation of the <see cref="IChannelManager"/>.
	/// Contains methods that enable the creation of channels that implement
	/// specific service interfaces. This class is designed to be used with
	/// Unity in order for transparent factory production of service channels.
	/// This class can be used to provide Unity with a delegate
	/// for creating channels implementing the specified service interface.
	/// </summary>
	public class ChannelManagerSingleton : IChannelManager
	{
		Guid instanceId = Guid.NewGuid();

		#region Singleton implementation

		ChannelManagerSingleton()
		{
		}

		public static ChannelManagerSingleton Instance
		{
			get
			{
				return Nested.instance;
			}
		}

		class Nested
		{
			// Explicit static constructor to tell C# compiler
			// not to mark type as beforefieldinit
			static Nested()
			{
			}

			internal static readonly ChannelManagerSingleton instance = new ChannelManagerSingleton();
		}

		#endregion

		//readonly LoggerSingleton log = LoggerSingleton.Instance;

		readonly Dictionary<Type, object> channels = new Dictionary<Type, object>();
		/* LockRecursionPolicy.SupportsRecursion may not be required. It has been noticed 
		 * that recursion is occuring occasionally, and SupportsRecursion has been assigned 
		 * to prevent this problem. In short, it's a hack for now. */
		readonly ReaderWriterLockSlim channelsLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
		readonly Dictionary<DuplexChannelKey, object> duplexChannels = new Dictionary<DuplexChannelKey, object>();
		readonly ReaderWriterLockSlim duplexChannelsLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
		readonly string channelIdentifier = Assembly.GetExecutingAssembly().GetName().Name;

//		/// <summary>
//		/// Creates or retrieves a cached service channel.
//		/// <example>
//		/// var staticFactoryConfiguration = unityContainer.Configure&lt;IStaticFactoryConfiguration&gt;();
//		///	Debug.Assert(staticFactoryConfiguration != null);
//		///	if (staticFactoryConfiguration != null)
//		///	{
//		///		staticFactoryConfiguration.RegisterFactory&lt;IYourService&gt;(
//		///			ChannelManagerSingleton.Instance.GetChannel&lt;IYourService&gt;);
//		/// 
//		///		/* Attempt to retrieve and use the service. */
//		///		var yourService = Dependency.Resolve&lt;IYourService&gt;();
//		///		yourService.DoSomething();
//		/// }
//		/// </example>
//		/// </summary>
//		/// <param name="unityContainer">The unity container.</param>
//		/// <returns>A channel of the specified type.</returns>
//		/// <exception cref="CommunicationException">Occurs if the service implements <see cref="IServiceContract"/>, 
//		/// and the call to InitiateConnection results in a <code>CommunicationException</code></exception>
//		/// <exception cref="SecurityNegotiationException">Occurs if the service implements <see cref="IServiceContract"/>, 
//		/// and the call to InitiateConnection results in a <code>SecurityNegotiationException</code></exception>
//		public object GetChannel<TChannel>(IUnityContainer unityContainer)
//		{
//			return GetChannel<TChannel>();
//		}

		public TChannel GetChannel<TChannel>()
		{
			Type serviceType = typeof(TChannel);
			object service;

			bool cached;
			channelsLock.EnterUpgradeableReadLock();
			try
			{
				cached = channels.TryGetValue(serviceType, out service);
				if (!cached)
				{	/* Value not in cache, therefore we create it. */
					channelsLock.EnterWriteLock();
					try
					{
						cached = channels.TryGetValue(serviceType, out service);
						if (!cached)
						{
							BindingEndpointPair pair = null;
							codeEndpointsLock.EnterReadLock();
							try
							{
								codeEndpoints.TryGetValue(serviceType, out pair);
							}
							finally
							{
								codeEndpointsLock.ExitReadLock();
							}
							
							ChannelFactory<TChannel> channelFactory;

							/* We don't cache the factory as it contains a list of channels 
							 * that aren't removed if a fault occurs. */
							if (pair != null)
							{
								channelFactory = new ChannelFactory<TChannel>(
														pair.Binding, pair.Address);
							}
							else
							{
								channelFactory = new ChannelFactory<TChannel>("*");
							}

							service = channelFactory.CreateChannel();
							AddCustomHeaders((IClientChannel)service);

							var communicationObject = (ICommunicationObject)service;
							communicationObject.Faulted += OnSimplexChannelFaulted;
							communicationObject.Closing += OnSimplexChannelClosing;
							channels.Add(serviceType, service);
							communicationObject.Open();
							ConnectIfClientService(service, serviceType);

							if (Dependency.Initialized)
							{
								Dependency.Register<TChannel>((TChannel)service);
							}
						}
					}
					finally
					{
						channelsLock.ExitWriteLock();
					}
				}
			}
			finally
			{
				channelsLock.ExitUpgradeableReadLock();
			}

			if (cached)
			{
				try
				{
					ConnectIfClientService(service, serviceType);
				}
				catch (Exception ex)
				{
					ILog log = ResolveLog();
					if (log != null)
					{
						log.Warn("Possible timeout occured. Disconnecting and recreating channel " + serviceType, ex);
					}
					/* Disconnecting the channel will cause it to be removed from the cache 
					 * and thus recreated when GetChannel is next called. This effectively causes a retry. */
					DisconnectSimplexChannel((ICommunicationObject)service);

					return GetChannel<TChannel>();
				}
			}
			return (TChannel)service;
		}

		void AddCustomHeaders(IClientChannel channel)
		{
			MessageHeader shareableInstanceContextHeader = MessageHeader.CreateHeader(
				InstanceIdHeader.HeaderName,
				InstanceIdHeader.HeaderNamespace,
				instanceId.ToString());

			var scope = new OperationContextScope(channel);
			OperationContext.Current.OutgoingMessageHeaders.Add(shareableInstanceContextHeader);
		}

		public TChannel GetDuplexChannel<TChannel>(object callbackInstance)
		{
			ArgumentValidator.AssertNotNull(callbackInstance, "callbackInstance");

			Type serviceType = typeof(TChannel);
			object service;
			var key = new DuplexChannelKey { ServiceType = serviceType, CallBackInstance = callbackInstance };

			bool cached;
			duplexChannelsLock.EnterUpgradeableReadLock();
			try
			{
				cached = duplexChannels.TryGetValue(key, out service);
				if (!cached)
				{	/* Value not in cache, therefore we create it. */
					duplexChannelsLock.EnterWriteLock();
					try
					{
						cached = duplexChannels.TryGetValue(key, out service);
						if (!cached)
						{

							var context = new InstanceContext(callbackInstance);

							BindingEndpointPair pair = null;
							codeEndpointsLock.EnterReadLock();
							try
							{
								codeEndpoints.TryGetValue(serviceType, out pair);
							}
							finally
							{
								codeEndpointsLock.ExitReadLock();
							}
							
							DuplexChannelFactory<TChannel> channelFactory;

							/* We don't cache the factory as it contains a list of channels 
							 * that aren't removed if a fault occurs. */
							if (pair != null)
							{
								channelFactory = new DuplexChannelFactory<TChannel>(context, pair.Binding, pair.Address);
							}
							else
							{
								channelFactory = new DuplexChannelFactory<TChannel>(context, "*");
							}

							service = channelFactory.CreateChannel();
							AddCustomHeaders((IClientChannel)service);
							var communicationObject = (ICommunicationObject)service;
							communicationObject.Faulted += OnDuplexChannelFaulted;
							communicationObject.Closing += OnDuplexChannelClosing;

							duplexChannels.Add(key, service);
							try
							{
								communicationObject.Open();
							}
							catch (Exception ex)
							{
								throw new CommunicationException("Unable to open channel " + typeof(TChannel), ex);
							}

							ConnectIfClientService(service, serviceType);

							Dependency.Register<TChannel>((TChannel)service);
						}
					}
					finally
					{
						duplexChannelsLock.ExitWriteLock();
					}
				}
			}
			finally
			{
				duplexChannelsLock.ExitUpgradeableReadLock();
			}
					  
			if (cached)
			{
				/* Test that channel is still valid. */
				try
				{
					ConnectIfClientService(service, serviceType);
				}
				catch (Exception ex)
				{
					ILog log = ResolveLog();
					if (log != null)
					{
						log.Warn("Possible timeout occured. Disconnecting and recreating channel " + serviceType, ex);
					}
					/* Disconnecting the channel will cause it to be removed from the cache 
					 * and thus recreated when GetChannel is next called. This effectively causes a retry. */
					DisconnectDuplexChannel((ICommunicationObject)service);

					return GetDuplexChannel<TChannel>(callbackInstance);
				}
			}
			return (TChannel)service;
		}

		/// <summary>
		/// Attempts to excercise the <code>IServiceContract.InitiateConnection</code> method
		/// on the specified service if that service is an <see cref="IServiceContract"/>.
		/// </summary>
		/// <param name="service">The service to attempt a connection.</param>
		/// <param name="serviceType">Type of the service for logging purposes.</param>
		/// <exception cref="CommunicationException">Occurs if the service implements <see cref="IServiceContract"/>, 
		/// and the call to InitiateConnection results in a <code>CommunicationException</code></exception>
		/// <exception cref="SecurityNegotiationException">Occurs if the service implements <see cref="IServiceContract"/>, 
		/// and the call to InitiateConnection results in a <code>SecurityNegotiationException</code></exception>
		void ConnectIfClientService(object service, Type serviceType)
		{
			var clientService = service as IServiceContract;
			if (clientService != null)
			{
				ILog log = ResolveLog();

				string response;
				try
				{
					response = clientService.InitiateConnection(channelIdentifier);
				}
				catch (Exception ex)
				{
					
					log?.Warn("Unable to connect to IServiceContract instance. Type: " + serviceType, ex);

					throw;
				}

				log?.Info(string.Format("Received response from {0} \"{1}\"", serviceType, response));
			}
		}

		void OnSimplexChannelClosing(object sender, EventArgs e)
		{
			ILog log = ResolveLog();
			log?.Debug("Simplex channel closed.");
			DisconnectSimplexChannel((ICommunicationObject)sender);
		}

		/// <summary>
		/// Called when a channel fault occurs. The channel is removed from the channels cache
		/// so that the next request to retrieve the channel will see it recreated.
		/// </summary>
		/// <param name="sender">The <see cref="ICommunicationObject"/> sender.</param>
		/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		void OnSimplexChannelFaulted(object sender, EventArgs e)
		{
			ILog log = ResolveLog();
			log?.Warn("Simplex channel faulted " + sender);
			DisconnectSimplexChannel((ICommunicationObject)sender);
		}

		void DisconnectSimplexChannel(ICommunicationObject communicationObject)
		{
			channelsLock.EnterWriteLock();
			try
			{
				communicationObject.Faulted -= OnSimplexChannelFaulted;
				communicationObject.Closing -= OnSimplexChannelClosing;

				try
				{
					communicationObject.Abort();
				}
				catch (Exception ex)
				{
					ILog log = ResolveLog();
					log?.Warn("Unable to abort faulted service.", ex);
				}

				var keys = from pair in channels
						   where pair.Value == communicationObject
						   select pair.Key;

				/* Remove all items matching the channel. 
				 * This is somewhat defensive as there should only be one instance 
				 * of the channel in the channel dictionary. */
				foreach (var key in keys.ToList())
				{
					channels.Remove(key);
				}
			}
			finally
			{
				channelsLock.ExitWriteLock();
			}
		}

		void OnDuplexChannelClosing(object sender, EventArgs e)
		{
			ILog log = ResolveLog();
			log?.Debug("Duplex channel closed.");
			DisconnectDuplexChannel((ICommunicationObject)sender);
		}

		void OnDuplexChannelFaulted(object sender, EventArgs e)
		{
			ILog log = ResolveLog();
			log?.Warn("Duplex channel faulted.");
			DisconnectDuplexChannel((ICommunicationObject)sender);
		}

		void DisconnectDuplexChannel(ICommunicationObject communicationObject)
		{
			duplexChannelsLock.EnterWriteLock();
			try
			{
				communicationObject.Faulted -= OnDuplexChannelFaulted;
				communicationObject.Closing -= OnDuplexChannelClosing;

				try
				{
					communicationObject.Abort();
				}
				catch (Exception ex)
				{
					ILog log = ResolveLog();
					log?.Warn("Unable to abort faulted service.", ex);
				}

				var keys = from pair in duplexChannels
						   where pair.Value == communicationObject
						   select pair.Key;

				/* Remove all items matching the channel. 
				 * This is somewhat defensive as there should only be one instance 
				 * of the channel in the channel dictionary. */
				foreach (var key in keys.ToList())
				{
					duplexChannels.Remove(key);
				}
			}
			finally
			{
				duplexChannelsLock.ExitWriteLock();
			}
		}

		class DuplexChannelKey
		{
			public object CallBackInstance { get; set; }
			public Type ServiceType { get; set; }

			public override bool Equals(object obj)
			{
				if (ReferenceEquals(null, obj))
				{
					return false;
				}
				if (ReferenceEquals(this, obj))
				{
					return true;
				}
				if (obj.GetType() != typeof(DuplexChannelKey))
				{
					return false;
				}
				return Equals((DuplexChannelKey)obj);
			}

			public bool Equals(DuplexChannelKey obj)
			{
				if (ReferenceEquals(null, obj))
				{
					return false;
				}
				if (ReferenceEquals(this, obj))
				{
					return true;
				}
				return Equals(obj.CallBackInstance, CallBackInstance) && Equals(obj.ServiceType, ServiceType);
			}

			public override int GetHashCode()
			{
				unchecked
				{
					return ((CallBackInstance != null ? CallBackInstance.GetHashCode() : 0) * 397)
						^ (ServiceType != null ? ServiceType.ToString().GetHashCode() : 0);
				}
			}

			public override string ToString()
			{
				if (CallBackInstance != null)
				{
					return string.Format("{0} {1}, CallBackInstance HashCode: {2}", 
						ServiceType, CallBackInstance, CallBackInstance.GetHashCode());
				}
				return string.Format("{0} {1}", ServiceType, CallBackInstance);
			}
		}

		#region Code Endpoints

		readonly Dictionary<Type, BindingEndpointPair> codeEndpoints = new Dictionary<Type, BindingEndpointPair>();
		readonly ReaderWriterLockSlim codeEndpointsLock = new ReaderWriterLockSlim();

		public void SetBinding(Type channelType, Binding binding, EndpointAddress endPointAddress)
		{
			ArgumentValidator.AssertNotNull(channelType, "channelType");
			ArgumentValidator.AssertNotNull(binding, "binding");
			ArgumentValidator.AssertNotNull(endPointAddress, "endPointAddress");

			codeEndpointsLock.EnterWriteLock();
			try
			{
				codeEndpoints[channelType] = new BindingEndpointPair(binding, endPointAddress);
			}
			finally
			{
				codeEndpointsLock.ExitWriteLock();
			}

			duplexChannelsLock.EnterWriteLock();
			try
			{
				foreach (var duplexChannel in duplexChannels.ToList())
				{
					if (duplexChannel.Key.ServiceType == channelType)
					{
						duplexChannels.Remove(duplexChannel.Key);
					}
				}
			}
			finally
			{
				duplexChannelsLock.ExitWriteLock();
			}

			channelsLock.EnterWriteLock();
			try
			{
				channels.Remove(channelType);
			}
			finally
			{
				channelsLock.ExitWriteLock();
			}
		}

		class BindingEndpointPair
		{
			public Binding Binding { get; private set; }
			public EndpointAddress Address { get; private set; }

			public BindingEndpointPair(Binding binding, EndpointAddress address)
			{
				Binding = binding;
				Address = address;
			}
		}

		#endregion

		static ILog ResolveLog()
		{
			ILog result;
			Dependency.TryResolve(out result);
			return result;
		}
	}
}
